package com.google.gwt.command.rebind;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.google.gwt.command.client.BindableCommand;
import com.google.gwt.command.client.Invoker;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.rebind.IndentedWriter;

public class InvokerManagerGenerator extends Generator {

	protected static final String IMPORT = "import %1$s;";
	protected static final String PACKAGE = "package %s;";

	protected JClassType interfaceType(TypeOracle oracle, String s,
			TreeLogger treeLogger) throws UnableToCompleteException {
		JClassType type;
		try {
			type = oracle.getType(s);
		} catch (NotFoundException e) {
			treeLogger.log(TreeLogger.ERROR, String.format(
					"%s: Could not find the interface [%s]. %s", e.getClass()
							.getName(), s, e.getMessage()));
			throw new UnableToCompleteException();
		}
		return type;
	}

	public String generate(TreeLogger treeLogger,
			GeneratorContext generatorContext, String s)
			throws UnableToCompleteException {
		JClassType invokerManagerClass = interfaceType(
				generatorContext.getTypeOracle(), s, treeLogger);
		// get the package name and building the implementation name
		String packageName = invokerManagerClass.getPackage().getName();
		String implName = invokerManagerClass.getName().replace(".", "_")
				+ "Impl";

		// initializing the class writer
		PrintWriterManager writers = new PrintWriterManager(generatorContext,
				treeLogger, packageName);
		PrintWriter printWriter = writers.tryToMakePrintWriterFor(implName);

		if (printWriter != null) {
			IndentedWriter writer = new IndentedWriter(printWriter);
			// writing the package line
			writer.write(String.format(PACKAGE, packageName));
			writer.newline();
			// generate the content
			try {
				doGenerate(invokerManagerClass, implName,
						generatorContext.getTypeOracle(), writer);
			} catch (NotFoundException e) {
				e.printStackTrace();
			}
			// commit the changes
			writers.commit();
		}
		return packageName + "." + implName;
	}

	protected void doGenerate(JClassType interfaceType, String implName,
			TypeOracle typeOracle, IndentedWriter writer)
			throws NotFoundException {
		List<JClassType> commands = findCommands(typeOracle);

		writeImports(writer);
		writeClassIntro(interfaceType, implName, writer);
		writeCommandMapField(commands, writer);
		writeHandlersField(writer);
		writeConstructor(implName, commands, writer, typeOracle);
		writeBindCommandsMethod(commands, typeOracle, writer);
		writeUnBindMethod(writer);
		writeGetManagedCommandsMethod(commands, typeOracle, writer);
		writeDispatchEvent(writer);
		writeClassOutro(writer);

	}

	private void writeUnBindMethod(IndentedWriter writer) {
		writer.write("public void unbindCommands() {");
		writer.indent();
		writer.write("if (handlers != null) {");
		writer.indent();
		writer.write("Iterator<HandlerRegistration> iterator = handlers.iterator();");
		writer.write("while (iterator.hasNext()) {");
		writer.indent();
		writer.write("HandlerRegistration handlerRegistration = (HandlerRegistration) iterator.next();");
		writer.write("handlerRegistration.removeHandler();");
		writer.outdent();
		writer.write("}");
		writer.outdent();
		writer.write("}");
		writer.outdent();
		writer.write("}");

	}

	private void writeHandlersField(IndentedWriter writer) {
		writer.newline();
		writer.write("private List<HandlerRegistration> handlers;");
	}

	private void writeConstructor(String implName, List<JClassType> commands,
			IndentedWriter writer, TypeOracle typeOracle)
			throws NotFoundException {
		writer.newline();
		writer.write("public %s(){", implName);
		writer.indent();
		for (JClassType command : commands) {
			writer.write("eventCommandMap.put(%s.getType(),new %s());",
					extractCommandEventTypeSupported(typeOracle, command)
							.getQualifiedSourceName(), command
							.getQualifiedSourceName());
		}
		writer.outdent();
		writer.write("}");
	}

	private void writeCommandMapField(List<JClassType> commands,
			IndentedWriter writer) {
		writer.newline();
		writer.indent();
		writer.write("private Map<Type<?>, Command<?>> eventCommandMap = new HashMap<Type<?>, Command<?>>();");
	}

	private void writeGetManagedCommandsMethod(List<JClassType> commands,
			TypeOracle typeOracle, IndentedWriter writer) {
		writer.newline();
		writer.write("public Map<Type<?>, Command<?>> getManagedCommands(){ return eventCommandMap;}");
	}

	private void writeDispatchEvent(IndentedWriter writer) {
		writer.newline();
		writer.write("public void dispatchEvent(GwtEvent<?> event,boolean isUnDo){");
		writer.indent();
		writer.write("Command command = (Command)eventCommandMap.get(event.getAssociatedType());");
		writer.write("if(isUnDo){");
		writer.indent();
		writer.write("command.undo(event);");
		writer.outdent();
		writer.write("} else {");
		writer.indent();
		writer.write("command.execute(event);");
		writer.outdent();
		writer.write("}");
		writer.outdent();
		writer.write("}");
	}

	private void writeBindCommandsMethod(List<JClassType> commands,
			TypeOracle typeOracle, IndentedWriter writer)
			throws NotFoundException {
		writer.newline();
		writer.write("public void bindCommands(EventBus eventBus,final Invoker invoker){");
		writer.indent();
		writer.write("handlers = new ArrayList<HandlerRegistration>();");
		for (JClassType command : commands) {
			JClassType eventClassType = extractCommandEventTypeSupported(
					typeOracle, command);
			String eventName = eventClassType.getQualifiedSourceName();
			JClassType handlerClassType = getHandlerClassType(typeOracle,
					command);
			String handlerName = handlerClassType.getQualifiedSourceName();
			Object handlerMethodName = handlerClassType.getMethods()[0]
					.getName();
			writer.write(
					"handlers.add(eventBus.addHandler(%s.getType(), new %s() {",
					eventName, handlerName);
			writer.newline();
			writer.indent();
			writer.write("public void %s(%s event) {", handlerMethodName,
					eventName);
			writer.indent();
			writer.write("invoker.run(event,false);");
			writer.outdent();
			writer.write("}");
			writer.outdent();
			writer.write("}));");
		}
		writer.newline();
		writer.outdent();
		writer.write("}");

	}

	private JClassType getHandlerClassType(TypeOracle typeOracle,
			JClassType command) throws NotFoundException {
		JClassType eventClassType = extractCommandEventTypeSupported(
				typeOracle, command);
		JClassType handlerClassType = eventClassType.getNestedTypes()[0];
		return handlerClassType;
	}

	private JClassType extractCommandEventTypeSupported(TypeOracle typeOracle,
			JClassType jClassType) throws NotFoundException {
		JType eventType = jClassType.getImplementedInterfaces()[0].getMethods()[0]
				.getParameterTypes()[0];
		JClassType eventClassType = typeOracle.getType(eventType
				.getQualifiedSourceName());
		return eventClassType;
	}

	private List<JClassType> findCommands(TypeOracle typeOracle) {
		List<JClassType> commands = new ArrayList<JClassType>();
		for (JClassType type : typeOracle.getTypes()) {
			BindableCommand annotation = type
					.getAnnotation(BindableCommand.class);
			if (annotation != null) {
				commands.add(type);
			}
		}
		return commands;
	}

	private void writeImports(IndentedWriter writer) {
		writer.write(IMPORT, HandlerRegistration.class.getName());
		writer.write(IMPORT, Map.class.getName());
		writer.write(IMPORT, HashMap.class.getName());
		writer.write(IMPORT, GwtEvent.class.getName());
		writer.write(IMPORT, Iterator.class.getName());
		writer.write(IMPORT, List.class.getName());
		writer.write(IMPORT, ArrayList.class.getName());
		writer.write(IMPORT, EventBus.class.getName());
		writer.write(IMPORT, Invoker.class.getName());
		writer.write(IMPORT, Entry.class.getName().replace("$", "."));
		writer.write(IMPORT, GwtEvent.Type.class.getName().replace("$", "."));
		writer.newline();
	}

	protected void writeClassIntro(JClassType interfaceType, String implName,
			IndentedWriter writer) {
		writer.write("public class %1$s implements %2$s {", implName,
				interfaceType.getName());
	}

	protected JParameter[] extractInterfaceMethodParams(JClassType interfaceType) {
		return interfaceType.getImplementedInterfaces()[0].getMethods()[0]
				.getParameters();
	}

	protected void writeClassOutro(IndentedWriter writer) {
		writer.outdent();
		writer.write("}");
	}

}
